import socket
import sys
import concurrent.futures
import time
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import threading
import ipaddress

class PortScannerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("端口连通性检测工具")
        self.root.geometry("800x600")
        self.root.minsize(800, 600)
        
        self.scan_in_progress = False
        self.stop_scan_flag = False
        
        self.create_widgets()
        self.center_window()
        
    def center_window(self):
        """将窗口居中显示"""
        self.root.update_idletasks()
        width = self.root.winfo_width()
        height = self.root.winfo_height()
        x = (self.root.winfo_screenwidth() // 2) - (width // 2)
        y = (self.root.winfo_screenheight() // 2) - (height // 2)
        self.root.geometry(f'{width}x{height}+{x}+{y}')
        
    def create_widgets(self):
        """创建GUI界面组件"""
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # IP地址和端口设置区域
        settings_frame = ttk.LabelFrame(main_frame, text="扫描设置", padding="10")
        settings_frame.pack(fill=tk.X, pady=5)
        
        # IP地址设置
        ttk.Label(settings_frame, text="目标IP地址:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        self.ip_entry = ttk.Entry(settings_frame, width=20)
        self.ip_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
        self.ip_entry.insert(0, "127.0.0.1")
        
        # 端口设置
        ttk.Label(settings_frame, text="端口设置:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
        self.port_mode = tk.StringVar(value="single")
        
        port_mode_frame = ttk.Frame(settings_frame)
        port_mode_frame.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
        
        ttk.Radiobutton(port_mode_frame, text="单个端口", variable=self.port_mode, value="single",
                        command=self.toggle_port_mode).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Radiobutton(port_mode_frame, text="端口范围", variable=self.port_mode, value="range",
                        command=self.toggle_port_mode).pack(side=tk.LEFT)
        
        # 单个端口输入
        self.single_port_frame = ttk.Frame(settings_frame)
        self.single_port_frame.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
        
        ttk.Label(self.single_port_frame, text="端口号:").pack(side=tk.LEFT, padx=5)
        self.port_entry = ttk.Entry(self.single_port_frame, width=10)
        self.port_entry.pack(side=tk.LEFT, padx=5)
        self.port_entry.insert(0, "80")
        
        # 端口范围输入
        self.range_port_frame = ttk.Frame(settings_frame)
        self.range_port_frame.grid(row=3, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
        self.range_port_frame.grid_remove()  # 默认隐藏
        
        ttk.Label(self.range_port_frame, text="起始端口:").pack(side=tk.LEFT, padx=5)
        self.start_port_entry = ttk.Entry(self.range_port_frame, width=10)
        self.start_port_entry.pack(side=tk.LEFT, padx=5)
        self.start_port_entry.insert(0, "80")
        
        ttk.Label(self.range_port_frame, text="结束端口:").pack(side=tk.LEFT, padx=5)
        self.end_port_entry = ttk.Entry(self.range_port_frame, width=10)
        self.end_port_entry.pack(side=tk.LEFT, padx=5)
        self.end_port_entry.insert(0, "100")
        
        # 高级设置
        advanced_frame = ttk.LabelFrame(settings_frame, text="高级设置", padding="10")
        advanced_frame.grid(row=4, column=0, columnspan=2, sticky=tk.EW, padx=5, pady=10)
        
        ttk.Label(advanced_frame, text="超时时间(秒):").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        self.timeout_var = tk.DoubleVar(value=2.0)
        timeout_spinbox = ttk.Spinbox(advanced_frame, from_=0.1, to=10.0, increment=0.1,
                                      textvariable=self.timeout_var, width=5)
        timeout_spinbox.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
        
        ttk.Label(advanced_frame, text="并发线程数:").grid(row=0, column=2, sticky=tk.W, padx=5, pady=5)
        self.workers_var = tk.IntVar(value=10)
        workers_spinbox = ttk.Spinbox(advanced_frame, from_=1, to=100, increment=1,
                                     textvariable=self.workers_var, width=5)
        workers_spinbox.grid(row=0, column=3, sticky=tk.W, padx=5, pady=5)
        
        # 按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X, pady=10)
        
        self.scan_button = ttk.Button(button_frame, text="开始扫描", command=self.start_scan)
        self.scan_button.pack(side=tk.LEFT, padx=5)
        
        self.stop_button = ttk.Button(button_frame, text="停止扫描", command=self.stop_scan, state=tk.DISABLED)
        self.stop_button.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(button_frame, text="清空结果", command=self.clear_results).pack(side=tk.LEFT, padx=5)
        
        # 扫描结果展示区域
        result_frame = ttk.LabelFrame(main_frame, text="扫描结果", padding="10")
        result_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        
        # 创建结果显示的表格
        self.create_result_table(result_frame)
        
        # 状态栏
        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # 进度条
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, length=100, mode='determinate',
                                           variable=self.progress_var)
        self.progress_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=5)
        
    def create_result_table(self, parent):
        """创建结果表格"""
        # 创建表格Frame
        table_frame = ttk.Frame(parent)
        table_frame.pack(fill=tk.BOTH, expand=True)
        
        # 创建Treeview作为表格
        columns = ("ip", "port", "status", "response_time")
        self.result_table = ttk.Treeview(table_frame, columns=columns, show="headings")
        
        # 设置列标题
        self.result_table.heading("ip", text="IP地址")
        self.result_table.heading("port", text="端口")
        self.result_table.heading("status", text="状态")
        self.result_table.heading("response_time", text="响应时间(秒)")
        
        # 设置列宽
        self.result_table.column("ip", width=150)
        self.result_table.column("port", width=100)
        self.result_table.column("status", width=100)
        self.result_table.column("response_time", width=150)
        
        # 添加滚动条
        scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.result_table.yview)
        self.result_table.configure(yscroll=scrollbar.set)
        
        # 放置表格和滚动条
        self.result_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 统计信息框架
        stats_frame = ttk.Frame(parent)
        stats_frame.pack(fill=tk.X, pady=10)
        
        self.total_ports_var = tk.StringVar(value="总端口数: 0")
        self.open_ports_var = tk.StringVar(value="开放端口: 0")
        self.closed_ports_var = tk.StringVar(value="关闭端口: 0")
        
        ttk.Label(stats_frame, textvariable=self.total_ports_var).pack(side=tk.LEFT, padx=10)
        ttk.Label(stats_frame, textvariable=self.open_ports_var).pack(side=tk.LEFT, padx=10)
        ttk.Label(stats_frame, textvariable=self.closed_ports_var).pack(side=tk.LEFT, padx=10)
    
    def toggle_port_mode(self):
        """切换端口模式（单个端口/端口范围）"""
        if self.port_mode.get() == "single":
            self.single_port_frame.grid()
            self.range_port_frame.grid_remove()
        else:
            self.single_port_frame.grid_remove()
            self.range_port_frame.grid()
    
    def start_scan(self):
        """开始扫描端口"""
        if self.scan_in_progress:
            return
        
        # 获取IP地址
        ip = self.ip_entry.get().strip()
        
        # 验证IP地址
        try:
            ipaddress.ip_address(ip)
        except ValueError:
            messagebox.showerror("错误", "无效的IP地址格式")
            return
        
        # 获取端口
        ports = []
        if self.port_mode.get() == "single":
            try:
                port = int(self.port_entry.get())
                if port < 1 or port > 65535:
                    raise ValueError
                ports = [port]
            except ValueError:
                messagebox.showerror("错误", "端口号必须是1-65535之间的整数")
                return
        else:
            try:
                start_port = int(self.start_port_entry.get())
                end_port = int(self.end_port_entry.get())
                
                if start_port < 1 or start_port > 65535 or end_port < 1 or end_port > 65535:
                    raise ValueError("端口范围无效")
                    
                if start_port > end_port:
                    raise ValueError("起始端口不能大于结束端口")
                    
                if end_port - start_port > 1000:
                    if not messagebox.askyesno("警告", "扫描超过1000个端口可能需要较长时间，是否继续?"):
                        return
                
                ports = list(range(start_port, end_port + 1))
            except ValueError as e:
                messagebox.showerror("错误", str(e) if str(e) != "invalid literal for int() with base 10: ''" else "请输入有效的端口范围")
                return
        
        # 获取高级设置
        timeout = self.timeout_var.get()
        workers = self.workers_var.get()
        
        # 清空表格
        for item in self.result_table.get_children():
            self.result_table.delete(item)
        
        # 重置统计信息
        self.total_ports_var.set(f"总端口数: {len(ports)}")
        self.open_ports_var.set("开放端口: 0")
        self.closed_ports_var.set("关闭端口: 0")
        
        # 设置状态
        self.scan_in_progress = True
        self.stop_scan_flag = False
        self.scan_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)
        
        # 启动扫描线程
        scan_thread = threading.Thread(target=self.perform_scan, args=(ip, ports, timeout, workers))
        scan_thread.daemon = True
        scan_thread.start()
    
    def stop_scan(self):
        """停止扫描"""
        if self.scan_in_progress:
            self.stop_scan_flag = True
            self.status_var.set("正在停止扫描...")
    
    def perform_scan(self, ip, ports, timeout, max_workers):
        """执行端口扫描"""
        self.status_var.set(f"正在扫描IP {ip} 的 {len(ports)} 个端口...")
        self.progress_var.set(0)
        
        open_ports = 0
        closed_ports = 0
        scanned_count = 0
        
        # 使用线程池进行并发扫描
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
            future_to_port = {executor.submit(self.check_port, ip, port, timeout): port for port in ports}
            
            for future in concurrent.futures.as_completed(future_to_port):
                if self.stop_scan_flag:
                    executor.shutdown(wait=False, cancel_futures=True)
                    break
                
                port = future_to_port[future]
                try:
                    is_open, response_time = future.result()
                    
                    # 更新统计信息
                    if is_open:
                        status = "开放"
                        open_ports += 1
                        # 对开放端口使用绿色
                        self.result_table.insert("", tk.END, values=(ip, port, status, f"{response_time:.3f}"),
                                                tags=("open",))
                    else:
                        status = "关闭"
                        closed_ports += 1
                        # 对关闭端口使用灰色
                        self.result_table.insert("", tk.END, values=(ip, port, status, f"{response_time:.3f}"),
                                                tags=("closed",))
                    
                    # 更新UI
                    scanned_count += 1
                    progress = (scanned_count / len(ports)) * 100
                    
                    self.root.after(0, self.update_progress, progress)
                    self.root.after(0, self.update_stats, open_ports, closed_ports)
                    
                except Exception as e:
                    self.root.after(0, lambda: self.result_table.insert("", tk.END, 
                                                                      values=(ip, port, "错误", str(e))))
        
        # 扫描完成后更新UI
        if self.stop_scan_flag:
            self.root.after(0, lambda: self.status_var.set("扫描已停止"))
        else:
            self.root.after(0, lambda: self.status_var.set("扫描完成"))
        
        self.root.after(0, self.update_stats, open_ports, closed_ports)
        self.root.after(0, self.reset_scan_state)
    
    def update_progress(self, progress):
        """更新进度条"""
        self.progress_var.set(progress)
    
    def update_stats(self, open_ports, closed_ports):
        """更新统计信息"""
        self.open_ports_var.set(f"开放端口: {open_ports}")
        self.closed_ports_var.set(f"关闭端口: {closed_ports}")
    
    def reset_scan_state(self):
        """重置扫描状态"""
        self.scan_in_progress = False
        self.scan_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        
        # 设置表格行颜色
        self.result_table.tag_configure("open", background="#E0FFE0")  # 浅绿色
        self.result_table.tag_configure("closed", background="#F0F0F0")  # 浅灰色
    
    def check_port(self, ip, port, timeout):
        """检测指定IP和端口是否可以连通"""
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        
        start_time = time.time()
        
        try:
            result = sock.connect_ex((ip, port))
            response_time = time.time() - start_time
            
            if result == 0:
                return True, response_time
            return False, response_time
        except socket.error:
            response_time = time.time() - start_time
            return False, response_time
        finally:
            sock.close()
    
    def clear_results(self):
        """清空扫描结果"""
        for item in self.result_table.get_children():
            self.result_table.delete(item)
        
        self.total_ports_var.set("总端口数: 0")
        self.open_ports_var.set("开放端口: 0")
        self.closed_ports_var.set("关闭端口: 0")
        self.progress_var.set(0)
        self.status_var.set("就绪")

def main():
    root = tk.Tk()
    app = PortScannerApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()